<?php
require_once 'PEAR/Exception.php';
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4 foldmethod=marker: */
/**
 * class definition of XML/EncryptedDomDocument
 *
 * PHP version 5
 *
 * LICENSE: This source file is subject to version 3.0 of the PHP license
 * that is available through the world-wide-web at the following URI:
 * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
 * the PHP License and are unable to obtain it through the web, please
 * send a note to license@php.net so we can mail you a copy immediately.
 *
 * @category    XML
 * @package     XML_EncryptedDomDocument
 * @author      Josh Peters <jcpeters@gmail.com>
 * @copyright   2006 Josh Peters
 * @license     http://www.opensource.org/licenses/bsd-license.php BSD License
 * @version     0.6
 * @link        http://pear.php.net/package/XML_EncryptedDomDocument
 */
// {{{ XML_EncryptedDomDocument
/**
 * supports a subset of the XML Encryption recommendation
 *
 * XML_EncryptedDomDocument aims to add the functionality of the XML Encryption
 * recommendation to the PHP 5 DomDocument class.  A program making use of
 * XML_EncryptedDomDocument should find encrypting a document or portion of
 * that document a relatively simple task. Decryption should also be abstracted
 * away so that a developer never needs to know anything about XML Encryption.
 * The class should be relatively transparent so that its functionality can be
 * taken advantage of without relearning everything about the DOM.
 * Allows for the decryption of a document containing a predetermined key.
 * Allows for the encrypting of a document using a predetermined key and an
 * inline CipherValue element.
 *
 * @category    XML
 * @package     XML_EncryptedDomDocument
 * @author      Josh Peters <jcpeters@gmail.com>
 * @copyright   2006 Josh Peters
 * @license     http://www.opensource.org/licenses/bsd-license.php BSD License
 * @version     0.6
 * @link        http://pear.php.net/package/XML_EncryptedDomDocument
 */
class XML_EncryptedDomDocument extends DomDocument
{
    /*
     * constants:
     * so far only a handful are used, but all are provided for reference purposes
     * prefixes are as follows:
     * NS_            denotes a namespace
     * Type_        denotes an EncryptedData's type
     * BLOCK_        denotes a block cipher
     * Encoding_    denotes an Transport encoding
     * Agree_        denotes a key agreement
     * KeyWrap_        denotes a key wrap
     * Digest_        denotes a digest algorithm
     * Auth_        denotes an authentication type
     * Canon_        denotes canonical XML URIs
     */
    // {{{ class constants
    /**
     * URI of the encryption namespace
     */
    const NS_ENCRYPTION         = 'http://www.w3.org/2001/04/xmlenc#';
    /**
     * URI of the digital signature namespace
     */
    const NS_SIGNATURE          = 'http://www.w3.org/2000/09/xmldsig#';

    /**
     * URI of EncryptedData's possible Type attribute; denotes an element
     */
    const TYPE_ELEMENT          = 'http://www.w3.org/2001/04/xmlenc#Element';
    /**
     * URI of EncryptedData's possible Type attribute; denotes content only
     */
    const TYPE_CONTENT          = 'http://www.w3.org/2001/04/xmlenc#Content';
    
    /**
     * URI of Block Cipher Triple DES
     */
    const BLOCK_3DES            = 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc';
    /**
     * URI of Block Cipher AES-128
     */
    const BLOCK_AES128          = 'http://www.w3.org/2001/04/xmlenc#aes128-cbc';
    /**
     * URI of Block Cipher AES-256
     */
    const BLOCK_AES256          = 'http://www.w3.org/2001/04/xmlenc#aes256-cbc';
    /**
     * URI of Block Cipher AES-192
     */
    const BLOCK_AES192          = 'http://www.w3.org/2001/04/xmlenc#aes192-cbc';

    /**
     * URI denoting base64-transformed content
     */
    const ENCODING_BASE64       = 'http://www.w3.org/2000/09/xmldsig#base64';
    /**
     * URI denoting XPath-transformed content
     */
    const ENCODING_XPATH        = 'http://www.w3.org/TR/1999/REC-xpath-19991116';

    /**
     * denotes usage of RSA 1.5 for transport
     */
    const TRANSPORT_RSA_1_5     = 'http://www.w3.org/2001/04/xmlenc#rsa-1_5';
    /**
     * denotes usage of OAEP for transport
     */
    const TRANSPORT_OAEP        = 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p';

    /**
     * denotes usage of Diffie-Hellman Key Agreement Protocol
     */
    const AGREE_DIFFIEHELLMAN   = 'http://www.w3.org/2001/04/xmlenc#dh';

    /**
     * denotes usage of Triple DES algorithm for Symmetric Key Wrap
     */
    const KEYWRAP_3DES          = 'http://www.w3.org/2001/04/xmlenc#kw-tripledes';
    /**
     * denotes usage of AES-128 algorithm for Symmetric Key Wrap
     */
    const KEYWRAP_AES128        = 'http://www.w3.org/2001/04/xmlenc#kw-aes128';
    /**
     * denotes usage of AES-256 algorithm for Symmetric Key Wrap
     */
    const KEYWRAP_AES256        = 'http://www.w3.org/2001/04/xmlenc#kw-aes256';
    /**
     * denotes usage of AES-192 algorithm for Symmetric Key Wrap
     */
    const KEYWRAP_AES192        = 'http://www.w3.org/2001/04/xmlenc#kw-aes192';

    /**
     * denotes usage of SHA-1 algorithm for Message Digest
     */
    const DIGEST_SHA1           = 'http://www.w3.org/2000/09/xmldsig#sha1';
    /**
     * denotes usage of SHA-256 algorithm for Message Digest
     */
    const DIGEST_SHA256         = 'http://www.w3.org/2001/04/xmlenc#sha256';
    /**
     * denotes usage of SHA-512 algorithmfor Message Digest
     */
    const DIGEST_SHA512         = 'http://www.w3.org/2001/04/xmlenc#sha512';
    /**
     * denotes usage of RIPEMD-160 algorithm for Message Digest
     */
    const DIGEST_RIPEMD160      = 'http://www.w3.org/2001/04/xmlenc#ripemd160';
    // }}}
    // {{{ properties
    /**
     * $cipherAlgorithms
     * a static lookup table providing us with the constants used by mcrypt
     */
    private static $cipherAlgorithms = array(
        XML_EncryptedDomDocument::BLOCK_3DES    => MCRYPT_3DES, 
        XML_EncryptedDomDocument::BLOCK_AES128  => MCRYPT_RIJNDAEL_128,
        XML_EncryptedDomDocument::BLOCK_AES256  => MCRYPT_RIJNDAEL_256,
        XML_EncryptedDomDocument::BLOCK_AES192  => MCRYPT_RIJNDAEL_192
    );
    /**
     * $defaultKey
     * if no id is specified we'll use this key for encryption/decryption
     */
    private $defaultKey = array();
    /**
     * $registeredKeys
     * a lookup consisting of IDs and their keys, used for encryption/decryption
     * allows a single XML_EncryptedDomDocument to encrypt/decrypt with
     * more than one key
     */
    private $registeredKeys = array();
    /**
     * $strictOutput
     * a flag denoting whether or not we should use setAttribute in addition to
     * setAttributeNS (to account for some clients that may not properly implement
     * namespaces)
     */
    private $strictOutput = true;
    /**
     * constructor
     * initialize parent and if no vectors are set yet create them
     */
    // }}}
    // {{{ setStrict
    /**
     * setStrict
     * Sets $strictOutput for this instance
     * @param boolean   the value set our $strictOutput parameter
     */
    public function setStrict($value = true)
    {
        $this->strictOutput = ($value == true);
    }
    // }}}
    // {{{ registerKey
    /**
     * registerKey
     * sets up the default key or associates a key to its $id
     * @param string some value
     * @param string an XML qualified name
	 * @return boolean true if key added, false if key already existed
     */
    public function registerKey($key, $algorithm = null, $id = null)
    {
        if ($id === null) {
			if ($this->defaultKey[$algorithm] !== null) {
                throw new XML_EncrypteDomDocumentException(
                            "default key already set for this algorithm!"
                        );
            }
            $ks = mcrypt_get_key_size(
                XML_EncryptedDomDocument::$cipherAlgorithms[$algorithm],
                'cbc'
            );
            if (strlen($key) != $ks) {
                throw new XML_EncrypteDomDocumentException(
                    "Expected key length of {$ks}, got " . strlen($key) . " instead"
                );
            }
            $this->defaultKey[$algorithm] = $key;
			return true;
        } else {
			if (! array_key_exists($algorithm, $this->registeredKeys)) {
                $this->registeredKeys[$algorithm] = array();
            }
            if (array_key_exists($id, $this->registeredKeys[$algorithm])) {
                throw new XML_EncrypteDomDocumentException(
                                "Key already set for this algorithm and ID!"
                            );
            }
            $this->registeredKeys[$algorithm][$id] = $key;
			return true;
        }
    }
    // }}}
    // {{{ encrpytDocument
    /**
     * encryptDocument
     * encrypts an entire DomDocument using @algorithm as its algorithm
     * and either $registeredKeys[@algorithm][@id] or $defaultKey as its key
     * sets the appropriate attributes for the resulting EncryptedData element
     * @param string a URI representing the algorithm to use
     * @param string a key in the $registeredKeys array
     */
    public function encryptDocument($algorithm, $id = null)
    {
        if (! isset(XML_EncryptedDomDocument::$cipherAlgorithms[$algorithm])) {
            throw new XML_EncrypteDomDocumentException('Unknown algorithm!');
        }
        $encryptedNode = $this->encryptNodeInline(
                            $this->documentElement,
                            $algorithm, $id
                        );
        $encryptedNode->setAttributeNS(
                            XML_EncryptedDomDocument::NS_ENCRYPTION,
                            "MimeType",
                            "text/xml"
                        );
        if (! $this->strictOutput) {
            $encryptedNode->setAttribute("MimeType", 'text/xml');
        }
    }
    // }}}
    // {{{ encryptNodeInline
    /**
     * encryptNodeInline
     * replaces $node with an encrypted version of it
     * @param DomNode a DomNode to encrypt
     * @param string a URI representing the algorithm to use
     * @param string a key in the $registeredKeys array,
     * will be associated with the EncryptedData element that is created
     * @return DomElement the resulting EncryptedData element
     */
    public function encryptNodeInline($node, $algorithm, $id = null)
    {
        $cipherXML = $this->encryptText($algorithm, $this->saveXML($node), $id);
        $encryptedNode = $this->createElementNS(
                                    XML_EncryptedDomDocument::NS_ENCRYPTION,
                                    "EncryptedData"
                                );
        if ($id !== null) {
            // set an ID for future use
            $encryptedNode->setAttributeNS(
                                XML_EncryptedDomDocument::NS_ENCRYPTION,
                                "Id",
                                $id
                            );
            if (! $this->strictOutput) {
                $encryptedNode->setAttribute("Id", $id);
            }
        }
        $methodNode = $this->createElementNS(
                                XML_EncryptedDomDocument::NS_ENCRYPTION,
                                "EncryptionMethod"
                            );
        $methodNode->setAttributeNS(
                            XML_EncryptedDomDocument::NS_ENCRYPTION,
                            "Algorithm",
                            $algorithm
                        );
        if (! $this->strictOutput) {
            $methodNode->setAttribute("Algorithm", $algorithm);
        }
        $cipherData = $this->createElementNS(
                                XML_EncryptedDomDocument::NS_ENCRYPTION,
                                "CipherData"
                            );
        $cipherValue = $this->createElementNS(
                                XML_EncryptedDomDocument::NS_ENCRYPTION,
                                "CipherValue"
                            );
        $cipherValue->appendChild($this->createTextNode($cipherXML));
        $cipherData->appendChild($cipherValue);
        $encryptedNode->appendChild($methodNode);
        $encryptedNode->appendChild($cipherData);

        $encryptedNode->setAttributeNS(
                            XML_EncryptedDomDocument::NS_ENCRYPTION,
                            "Type",
                            XML_EncryptedDomDocument::TYPE_ELEMENT
                        );
        if (! $this->strictOutput) {
            $encryptedNode->setAttribute("Type", XML_EncryptedDomDocument::TYPE_ELEMENT);
        }

        $node->parentNode->replaceChild(
            $encryptedNode,
            $node
        );
        return $encryptedNode;
    }
    // }}}
    // {{{ encryptNodeContents
    /**
     * encryptNodeContents
     * takes a text node and wraps an encrypted version of it with the appropriate elements
     * @param DomNode node to encrypt
     * @param string a URI representing the algorithm to use
     * @param string a key in the $registeredKeys array
     * @return DomElement a DomElement representing the encrypted version of the text node
     * @throws an XML_EncrypteDomDocumentException if the incoming node is not a text node
     */
    public function encryptNodeContents($node, $algorithm, $id)
    {
        if ($node->nodeName != '#text') {
            throw new XML_EncrypteDomDocumentException(
                        "Cannot call encryptNodeContents on non-text node"
                    );
        }
        $encryptedNode = $this->encryptNodeInline($node, $algorithm, $id);
        $encryptedNode->setAttributeNS(
                            XML_EncryptedDomDocument::NS_ENCRYPTION,
                            "Type",
                            XML_EncryptedDomDocument::TYPE_CONTENT
                        );
        if (! $this->strictOutput) {
            $encryptedNode->setAttribute(
                                "Type",
                                XML_EncryptedDomDocument::TYPE_CONTENT
                            );
        }
        return $encryptedNode;
    }
    // }}}
    // {{{ encryptText
    /**
     * encryptText
     * looks up the cipher from the $cipherAlgorithms array and encodes $plainText
     * @param string a URI representing the algorithm to use
     * @param string the plaintext to encrypt
     * @param string a key in the $registeredKeys array
     * @return string a base64 encoded string corresponding to the resulting ciphertext
     */
    public function encryptText($algorithm, $plainText, $id = null)
    {
		$cipher = mcrypt_module_open(
                    XML_EncryptedDomDocument::$cipherAlgorithms[$algorithm],
                    null,
                    MCRYPT_MODE_CBC,
                    null
                );
        $ivSize = mcrypt_enc_get_iv_size($cipher);
		$iv = mcrypt_create_iv($ivSize, MCRYPT_RAND);
		$e = mcrypt_generic_init(
            $cipher,
            (
                $id === null
                ?
                $this->defaultKey[$algorithm]
                :
                $this->registeredKeys[$algorithm][$id]
            ),
            $iv
        );
        if ($e === false) {
            throw new XML_EncrypteDomDocumentException(
                            "MCRYPT: Incorrect Parameters!"
                        );
        } elseif ($e == -3) {
            throw new XML_EncrypteDomDocumentException(
                            "MCRYPT: Key Length Incorrect!"
                        );
        } elseif ($e == -4) {
            throw new XML_EncrypteDomDocumentException(
                            "MCRYPT: Memory Allocation Error!"
                        );
        } elseif ($e < 0) {
            throw new XML_EncrypteDomDocumentException(
                            "MCRYPT: Unknown Decryption Error Code ({$e})!"
                        );
        }
		$cipherText = mcrypt_generic($cipher, $plainText);
		mcrypt_generic_deinit($cipher);
        mcrypt_module_close($cipher);
        return base64_encode($iv.$cipherText);
    }
    // }}}
    // {{{ decryptDocument
    /**
     * decryptDocument
     * decrypts each EncryptedData node found in document via a call to decryptNode
     */
    public function decryptDocument()
    {
        // decrypt entire document
        $containers = $this->getElementsByTagNameNS(
                            XML_EncryptedDomDocument::NS_ENCRYPTION,
                            "EncryptedData"
                        );
        $this->decryptNode($this->documentElement);
    }
    // }}}
    // {{{ decryptNode
    /**
     * decryptNode
     * converts each CipherReference into a CipherValue
     * decrypts each CipherValue
     * replaces @node with a decrypted version of its contents
     * potentially recurses if a node is doubly encrypted
     * @param DomNode a DomNode to decrypt
	 * @return boolean false if decryption wasn't needed, true otherwise
     */
    public function decryptNode($node)
    {
        // do some checking to see if our node is indeed something we should be decrypting...
        if (! strpos($node->nodeName, 'EncryptedData')) {
            return false;
        }
        if (strpos($node->nodeName, ':') !== false) {
            list($prefix, $name) = explode(':', $node->nodeName, 2);
            if (
                $node->lookupNamespaceURI($prefix)
                !=
                XML_EncryptedDomDocument::NS_ENCRYPTION
            ) {
                return false;
            }
        } elseif (! $node->isDefaultNamespace(XML_EncryptedDomDocument::NS_ENCRYPTION)) {
            return false;
        }

        $plainText = '';
        $id = null;
        if (! ($id = $node->getAttributeNS(
                XML_EncryptedDomDocument::NS_ENCRYPTION, 'Id'))) {
            if (! ($id = $node->getAttribute('Id'))) {
                $id = null;
            }
        }
        foreach ($node->getElementsByTagNameNS(
            XML_EncryptedDomDocument::NS_ENCRYPTION, "CipherReference"
        ) as $cr) {
            $externalDoc = new XML_EncryptedDomDocument();
            $uri = '';
            $uri = $cr->getAttributeNS(
                        XML_EncryptedDomDocument::NS_ENCRYPTION,
                        'URI'
                    );
            if (trim($uri) == '') {
                $uri = $cr->getAttribute('URI');
                if (trim($uri) == '') {
                    throw new XML_EncrypteDomDocumentException(
                            'Required Attribute @URI not found!'
                        );
                }
            }
            $externalDoc->load($uri);
            foreach ($cr->getElementsByTagNameNS(
                XML_EncryptedDomDocument::NS_ENCRYPTION, 'Transforms'
            ) as $transforms) {
                $tempValue = '';
                foreach ($transforms->getElementsByTagNameNS(
                    XML_EncryptedDomDocument::NS_SIGNATURE, 'Transform'
                ) as $transform) {
                    switch ($transform->getAttribute('Algorithm')) {
                        case XML_EncryptedDomDocument::ENCODING_XPATH:
                            $XPath = new DomXPath($externalDoc);
                            foreach ($transform->getElementsByTagNameNS(
                                            XML_EncryptedDomDocument::NS_SIGNATURE,
                                            'XPath'
                                        ) as $query) {
                                foreach($XPath->query($query->nodeValue) as $qr) {
                                    $tempValue = $externalDoc->saveXML($qr);
                                }
                            }
                            unset($XPath);
                            break;
                        case XML_EncryptedDomDocument::ENCODING_BASE64:
                            $tempValue = base64_decode($tempValue);
                            break;
                        default:
                            throw new XML_EncrypteDomDocumentException(
                                "unknown transform algorithm:"
                                .
                                $transform->getAttribute('Algorithm')
                            );
                    }
                }
            }
            unset($externalDoc);
            // replacing CipherReference with a CipherValue
            $cv = $this->createElementNS(
                XML_EncryptedDomDocument::NS_ENCRYPTION, "CipherValue"
            );
            $cv->appendChild($this->createTextNode(base64_encode($tempValue)));
            $cr->parentNode->replaceChild($cv, $cr);
        }
        foreach ($node->getElementsByTagNameNS(
            XML_EncryptedDomDocument::NS_ENCRYPTION,
            "CipherValue"
        ) as $cv) {
            if ($node->getElementsByTagNameNS(
                XML_EncryptedDomDocument::NS_ENCRYPTION,
                'EncryptionMethod'
            )->length == 1) {
                $algorithm = $node->getElementsByTagNameNS(
                    XML_EncryptedDomDocument::NS_ENCRYPTION,
                    'EncryptionMethod'
                )->item(0)->getAttributeNS(
                    XML_EncryptedDomDocument::NS_ENCRYPTION,
                    'Algorithm'
                );
                $cipher = mcrypt_module_open(
                    XML_EncryptedDomDocument::$cipherAlgorithms[$algorithm],
                    null,
                    MCRYPT_MODE_CBC,
                    null
                );
                $ivSize = mcrypt_enc_get_iv_size($cipher);
                $cipherText = base64_decode($cv->nodeValue);
                $iv = substr($cipherText, 0, $ivSize);
                $cipherText = substr($cipherText, $ivSize);

                $e = mcrypt_generic_init(
                    $cipher, 
                    (
                        $id === null
                    ?
                        $this->defaultKey[$algorithm]
                    :
                        $this->registeredKeys[$algorithm][$id]
                    ),
                    $iv
                );
                if ($e === false) {
                    throw new XML_EncrypteDomDocumentException(
                                    "MCRYPT: Incorrect Parameters!"
                                );
                } elseif ($e == -3) {
                    throw new XML_EncrypteDomDocumentException(
                                    "MCRYPT: Key Length Incorrect!"
                                );
                } elseif ($e == -4) {
                    throw new XML_EncrypteDomDocumentException(
                                    "MCRYPT: Memory Allocation Error!"
                                );
                } elseif ($e < 0) {
                    throw new XML_EncrypteDomDocumentException(
                                    "MCRYPT: Unknown Error Code ({$e})!"
                                );
                }
                $plainText = mdecrypt_generic($cipher, $cipherText);
                mcrypt_generic_deinit($cipher);
                mcrypt_module_close($cipher);
            }
            $decipheredNode = null;
            if (trim($plainText) != '') {
                if (strpos($plainText, '<') === false) {
                    $decipheredNode = $this->createTextNode($plainText);
                } else {
                    try {
                        $d = new DomDocument();
                        $d->loadXML($plainText);
                        $decipheredNode = $this->importNode(
                                                $d->documentElement,
                                                true
                                            );
                        unset($d);
                    } catch (Exception $e) {
                        $decipheredNode = $this->createTextNode($e->getMessage());
                    }
                }
            }
            if ($decipheredNode instanceOf DomNode) {
                $node->parentNode->replaceChild(
                    $decipheredNode,
                    $node
                );
                if ($decipheredNode instanceOf DomElement) {
                    /*
                        we'll run decryptNode on the resulting node in case
                        it is an EncryptedData element
                    */
                    $this->decryptNode($decipheredNode);
                    /*
                        decrypt any internal EncryptedData elements we find
                        (with the correct namespace, of course...)
                    */
                    foreach ($decipheredNode->getElementsByTagNameNS(
                        XML_EncryptedDomDocument::NS_ENCRYPTION, 'EncryptedData'
                    ) as $newNode) {
                        $this->decryptNode($newNode);
                    }
                }
                return true;
            } else {
                return false;
            }
        }
        return true;
    }
    // }}}
}
// }}}
// {{{ XML_EncrypteDomDocumentException
class XML_EncrypteDomDocumentException extends PEAR_Exception
{
}
// }}}
?>
